/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2018-2020. All rights reserved.
 */

package com.huawei.datashelf.service.impl;

import com.huawei.datashelf.common.CommonFilter;
import com.huawei.datashelf.model.bo.CommonQuery;
import com.huawei.datashelf.model.bo.GroupQuery;
import com.huawei.datashelf.service.QueryService;
import com.huawei.datashelf.util.EsCommonUtil;
import com.huawei.tdia.core.exception.CustomerException;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.enums.SqlKeyword;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.SneakyThrows;

import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.aop.framework.Advised;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;

import javax.annotation.Resource;

/**
 * mysql查询通用服务
 *
 * @author lwx1155848
 * @since 2024-06-18
 */
@Slf4j
@Service("mysql")
public class MysqlQueryService implements QueryService, ApplicationRunner {
    private static final Map<String, Object> SERVICE_MAP = new HashMap<>();

    @Resource
    private ApplicationContext applicationContext;

    @Resource
    private ThreadPoolTaskExecutor asyncExecutor;

    @Override
    @SneakyThrows
    public IPage<Object> query(CommonQuery query) {
        Object model = Class.forName(query.getTableOrClass()).newInstance();
        QueryWrapper<Object> queryWrapper = new QueryWrapper<>(model);
        List<String> select = query.getSelect();
        if (CollUtil.isNotEmpty(select)) {
            List<String> fds = select.stream().map(item -> getDbFieldByName(model, item)).collect(Collectors.toList());
            queryWrapper.select(fds);
        }
        applyFilterCondition(queryWrapper, query.getFilter());
        Page<Object> page = new Page<>(query.getPageNum(), query.getPageSize());
        query.getOrders().forEach(item -> item.setColumn(getDbFieldByName(model, item.getColumn())));
        page.setOrders(query.getOrders());
        Object service = SERVICE_MAP.get(query.getTableOrClass());
        if (service == null) {
            throw new CustomerException("不存在模型对象:" + query.getTableOrClass());
        }
        ReflectUtil.invoke(service, "page", page, queryWrapper);
        return page;
    }

    @Override
    @SneakyThrows
    public JSONObject group(GroupQuery query) {
        Object model = Class.forName(query.getTableOrClass()).newInstance();
        Object service = SERVICE_MAP.get(query.getTableOrClass());
        if (service == null) {
            throw new CustomerException("不存在模型对象:" + query.getTableOrClass());
        }
        List<String> group = query.getGroup();
        Map<String, Page<String>> pageData = new ConcurrentHashMap<>();
        Map<String, List<String>> res = new ConcurrentHashMap<>();
        if (group.size() == 1) {
            String field = group.get(0);
            Page<String> page = getFieldOptions(model, service, field, query);
            res.put(field, page.getRecords());
            page.setRecords(null);
            pageData.put(field, page);
        } else {
            CountDownLatch latch = new CountDownLatch(group.size());
            for (String field : group) {
                asyncExecutor.execute(() -> {
                    try {
                        Page<String> page = getFieldOptions(model, service, field, query);
                        res.put(field, page.getRecords());
                        page.setRecords(null);
                        pageData.put(field, page);
                    } finally {
                        latch.countDown();
                    }
                });
            }
            latch.await();
        }
        JSONObject data = JSONUtil.parseObj(new TreeMap<>(res));
        data.set("page", pageData);
        return data;
    }

    @NotNull
    private static Page<String> getFieldOptions(Object model, Object service, String field, GroupQuery query) {
        String dbField = getDbFieldByName(model, field);
        CommonFilter filter = query.getFilter();
        QueryWrapper<Object> queryWrapper = new QueryWrapper<>(model);
        queryWrapper.select(dbField);
        if (filter != null) {
            applyFilterCondition(queryWrapper, filter);
        }
        Page<String> page = new Page<>(query.getPageNum(), query.getPageSize());
        page.setOrders(Collections.singletonList(OrderItem.desc(dbField)));
        queryWrapper.groupBy(dbField);
        Page<Object> resPage = ReflectUtil.invoke(service, "page", page, queryWrapper);
        boolean hasNull = false;
        List<String> group = new ArrayList<>();
        for (Object obj : resPage.getRecords()) {
            if (obj == null) {
                hasNull = true;
                continue;
            }
            group.add(String.valueOf(ReflectUtil.getFieldValue(obj, field)));
        }
        if (hasNull) {
            group.add(null);
        }
        page.setRecords(group);
        return page;
    }

    private static String getDbFieldByName(Object model, String fieldName) {
        Field field = ReflectUtil.getField(model.getClass(), fieldName);
        if (field == null) {
            throw new CustomerException("字段[" + fieldName + "]不存在!");
        }
        String dbFieldName = EsCommonUtil.getDbFieldName(field);
        if (StrUtil.isBlank(dbFieldName)) {
            throw new CustomerException("字段[" + fieldName + "]不存在!");
        }
        return dbFieldName;
    }

    private static void applyFilterCondition(QueryWrapper<Object> queryWrapper, CommonFilter filter) {
        applyFilterCondition(queryWrapper, filter, SqlKeyword.AND);
    }

    private static void applyFilterCondition(QueryWrapper<Object> queryWrapper, CommonFilter filter,
            SqlKeyword separator) {
        Object entity = queryWrapper.getEntity();
        if (filter.getFilterList().isEmpty()) {
            handleSingleFilter(queryWrapper, filter, separator, entity);
        } else {
            if (SqlKeyword.AND.equals(separator)) {
                queryWrapper.and(q -> {
                    for (CommonFilter subFilter : filter.getFilterList()) {
                        applyFilterCondition(q, subFilter, filter.getSeparator());
                    }
                });
            } else {
                queryWrapper.or(q -> {
                    for (CommonFilter subFilter : filter.getFilterList()) {
                        applyFilterCondition(q, subFilter, filter.getSeparator());
                    }
                });
            }
        }
    }

    private static void handleSingleFilter(QueryWrapper<Object> queryWrapper, CommonFilter filter, SqlKeyword separator,
            Object entity) {
        if (filter.getQueryType() == null) {
            return;
        }
        if (SqlKeyword.OR.equals(separator)) {
            queryWrapper.or();
        }
        String field = filter.getQueryField();
        Object value = filter.getQueryValue();
        String dbFieldByName = getDbFieldByName(entity, field);
        switch (filter.getQueryType()) {
            case EQ:
                queryWrapper.eq(dbFieldByName, value);
                break;
            case NE:
                queryWrapper.ne(dbFieldByName, value);
                break;
            case GT:
                queryWrapper.gt(dbFieldByName, value);
                break;
            case GE:
                queryWrapper.ge(dbFieldByName, value);
                break;
            case LT:
                queryWrapper.lt(dbFieldByName, value);
                break;
            case LE:
                queryWrapper.le(dbFieldByName, value);
                break;
            case LIKE:
                queryWrapper.like(dbFieldByName, value);
                break;
            case BETWEEN:
                List<String> list = (List<String>) value;
                queryWrapper.between(dbFieldByName, list.get(0), list.get(1));
                break;
            case IN:
                list = (List<String>) value;
                if (list.contains(null)) {
                    queryWrapper.and(query -> query.in(dbFieldByName, list).or().isNull(dbFieldByName));
                } else {
                    queryWrapper.in(dbFieldByName, list);
                }
                break;
            case NOT_IN:
                list = (List<String>) value;
                queryWrapper.notIn(dbFieldByName, list);
                break;
            case IS_NULL:
                queryWrapper.isNull(dbFieldByName);
                break;
            default:
                break;
        }
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Map<String, Object> services = applicationContext.getBeansWithAnnotation(Service.class);
        for (Object value : services.values()) {
            if (value instanceof Advised) {
                Advised advised = (Advised) value;
                Class<?> targetClass = advised.getTargetClass();
                if (targetClass != null) {
                    Type type = targetClass.getGenericSuperclass();
                    if (type instanceof ParameterizedType) {
                        ParameterizedType parameterizedType = (ParameterizedType) type;
                        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
                        if (actualTypeArguments.length == 2) {
                            Type modelType = actualTypeArguments[1];
                            SERVICE_MAP.put(modelType.getTypeName(), value);
                        }
                    }
                }
            }
        }
        log.info("init mysql SERVICE_MAP:{}",SERVICE_MAP);
    }
}
